Statement#

Suppose you have a list of weights and corresponding values for n items. You have a knapsack that can carry items up to a specific maximum weight, known as the capacity of the knapsack.

You want to maximize the sum of values of the items in your knapsack while ensuring that the sum of the weights of the items remains less than or equal to the knapsack’s capacity.

If all the combinations exceed the given knapsack’s capacity, then return 00.

Note: While adding items in the knapsack, we either add the complete item or don’t add it. Moreover, we can’t add an item again that is already in the bag.

Let’s say you have a knapsack capacity of 5 and a list of items with weights and values as follows:

weights = [1, 2, 3, 5]

values = [10, 5, 4, 8]

There are four ways of storing items in the knapsack, such that the combined weight of stored items is less than or equal to the knapsack’s capacity.

  • Item of weight 1 and weight 2, with a total value of 15.
  • Item of weight 1 and weight 3, with a total value of 14.
  • Item of weight 2 and weight 3, with a total value of 9.
  • Item of weight 5, with a value of 8.

Though all of the combinations described above are valid, we need to select the one with the maximum value. Hence, we will select items with weights 1 and 2, as they give us the maximum value of 15.

Constraints:

  • 11 \leq capacity 104\leq 10^4
  • 11 \leq values.length 103\leq 10^3
  • weights.length ==== values.length
  • 11 \leq values[i] 104\leq 10^4
  • 11 \leq weights[i] \leq capacity

Examples#

No.

capacity

weights

Values

n

Maximum Value

1

30

[10, 20, 30]

[22, 33, 44]

3

55

2

5

[1, 2, 3, 5]

[10, 5, 4, 8]

4

15

Try it yourself#

Implement your solution in the following playground.

Python
usercode > main.py
Input #1
Input #2
Input #3
Input #4
0/1 Knapsack

Note: If you clicked the “Submit” button and the code timed out, this means that your solution needs to be optimized in terms of running time.

Hint: Use dynamic programming and see the magic.

Solution#

We will first explore the naive recursive solution to this problem and then see how it can be improved using dynamic programming.

Naive approach#

A naive approach is to find all combinations of items such that their combined weight is less than the capacity of the knapsack and the total value is maximized.

For example, we have the following list of values and weights with the capacity of 55:

  • values: [3,5,2,7][3, 5, 2, 7]
  • weights: [3,1,2,4][3, 1, 2, 4]

To find the maximum value, we try all the possible combinations:

  • 3+53 + 5 (total weight 44) =8= 8
  • 3+23 + 2 (total weight 55) =5= 5
  • 5+25 + 2 (total weight 33) =7= 7
  • 5+75 + 7 (total weight 55) =12= 12

The calculation above shows that we need a recursive solution to make all possible combinations. In other words, we divide the problem into sub-problems, and for each item, if its weight is less than the capacity, we check whether we should place it in the knapsack.

Let’s implement the algorithm as discussed above:

0/1 Knapsack using recursion

Note: Please observe that if you include the test case commented out in the driver code, the solution is likely to time out. Try to solve the larger problem using the dynamic programming solutions provided below and see the difference.

Time complexity#

The time complexity of the naive approach is O(2n)O(2^n), where nn is the total number of items. This is because we have two possible choices every time, either to include the item or not.

Space complexity#

As the maximum depth of the recursive call tree is nn, and each call stores its data on the stack, the space complexity of this approach is O(n)O(n).

Optimized solution using dynamic programming#

Now, let’s improve the recursive solution using top-down and bottom-up approaches.

Top-down solution#

The top-down solution, commonly known as the memoization technique, is an enhancement of the recursive solution. It overcomes the problem of calculating redundant solutions over and over again by storing them in a table.

In the recursive approach, the two variables that kept changing in each call were the total weight of the knapsack and the number of items we had. So, we’ll use these two variables to define each distinct subproblem. Therefore, we need a 2-D array to store these values and the result of any given subproblem when we encounter it for the first time. At any later time, if we encounter the same sub-problem, we can return the stored result from the table with an O(1)O(1) lookup instead of recalculating that subproblem.

Let’s implement the algorithm as discussed above:

0/1 Knapsack using memoization

Note: Please observe that if you now uncomment the large test case in the driver code, the dynamic programming solution will still work.

Time complexity#

We avoided redundant calculations in this approach by storing all the intermediate results in a 2-D array in O(1)O(1) time. Therefore, the time complexity of this approach is reduced to O(n×capacity)O(n \times capacity), where nn is the number of items.

Space complexity#

We now need O(n×capacity)O(n \times capacity) space in memory to store intermediate values.

Bottom-up solution#

The bottom-up solution, also known as the tabulation technique, is an iterative method of solving dynamic programming problems. Here, we will create a 2-D array of size (n + 1) * (capacity + 1). The row indicates the values of the available items, and the column shows the capacity of the knapsack at any given point.

We will initialize the array so that when the row or column is 0, the value in the table will also be 0. Next, we will check if the weight of an item is less than the capacity. If yes, we have two options: either we can add the item to the knapsack, or we can skip it. If we include the item, the optimal solution is the maximum of the two values. Otherwise, if the weight of an item is greater than the capacity, then we don’t include the item in the knapsack.

The algorithm work as follows:

  • In the first row, we have no items to pick from, so regardless of the knapsack’s capacity, we can only accumulate a value of 0.

  • In the second row, we can only pick the first item. With a knapsack of capacity 1, we wouldn’t be able to pick this item. If the knapsack capacity is 2, we can pick this item. As the knapsack capacity increased beyond value[0], we couldn’t get any more value because there was only one item to choose from.

  • In the third row, we have the first two items available to be picked. For a knapsack of capacity c, we can either try to pick the second item or skip it. If we choose to skip an item, the value we can accumulate is the value one can obtain using only the first item and a knapsack of capacity c. This is already known and stored in dp[1][c]. If we decide to pick this item, it will take up a weight of weights[1] in the knapsack while contributing an additional value of values[1]. However, we need to track how much value we can accumulate with the remaining capacity in the knapsack, i.e., capacity - weights[1] while using only the first item. This value is already stored in dp[1][c-weights[1]]. So, the total value, in this case, becomes values[1] + dp[1][c-weights[1]]. Since we’re trying to maximize the value, we’ll pick the more valuable of the two choices, i.e., picking or not picking the second item.

  • In general, the cell in row i, column j can be filled with the following formula:

dp[i][j] = max(values[i-1]+ dp[i-1][j-weights[i-1]], dp[i-1][j])

Let’s look at the following illustration to get a better understanding of the solution:

Created with Fabric.js 3.6.6 For the base case, we make the first column equal to 0.

1 of 34

Created with Fabric.js 3.6.6 For the base case, we make the first row equal to 0.

2 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than the capacity, we will not pick it upand the value will remain the same of the previous row value.

3 of 34

Created with Fabric.js 3.6.6 Now the capacity is equal to the weight of the item, we will pickthis item value and the value will become 1.

4 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value and the value will become 1.

5 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value and the value will become 1.

6 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value and the value will become 1.

7 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value and the value will become 1.

8 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value and the value will become 1.

9 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value and the value will become 1.

10 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

11 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

12 of 34

Created with Fabric.js 3.6.6 Now the capacity is equal to the weight of the item, so we will decide if picking the second item gives us a maximum value or not.In this case, we will pick this item and the value will become 2.

13 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value and the value will become 2.

14 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by also keeping the previous item since we have capacity for both items.

15 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by also keeping the previous item since we have capacity for both items.

16 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by also keeping the previous item since we have capacity for both items.

17 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by also keeping the previous item since we have capacity for both items.

18 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

19 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

20 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

21 of 34

Created with Fabric.js 3.6.6 As the weight is equal to the capacity of the item, we will pickthis item value by removing the previous one since its value is less thanthe current item.

22 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by removing the previous one since its value is less thanthe current item.

23 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by keeping the previous one which has the weight of 2 as our capacity can contain both.

24 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by keeping the previous one which has the weight of 3 as our capacity can contain both.

25 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by keeping the previous one which has the weight of 3 as our capacity can contain both.

26 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

27 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

28 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

29 of 34

Created with Fabric.js 3.6.6 As the weight of the item is greater than capacity, we will not pick it upand the value will remain the same of the previous row value.

30 of 34

Created with Fabric.js 3.6.6 As the weight is equal to the capacity of the item, we will pickthis item value by removing the previous one since its value is less thanthe current item.

31 of 34

Created with Fabric.js 3.6.6 As the weight is equal to the capacity of the item, we will pickthis item value by removing the previous one since its value is less thanthe current item.

32 of 34

Created with Fabric.js 3.6.6 As the weight is less than the capacity of the item, we will pickthis item value by keeping the previous one which has the weight of 1 as our capacity can contain both.

33 of 34

Created with Fabric.js 3.6.6 Finally as the weight is less than the capacity of the item, we willpick this item value by keeping the previous one which has the weight of 1 as our capacity can contain both.

34 of 34

Let's implement the algorithm as discussed above:

0/1 Knapsack using tabulation

Note: Please observe that if you now uncomment the large test case in the driver code, the dynamic programming solution will still work.

Time complexity#

We have created a 2-D array to store the results of sub-problems that would be used later on, and filling each row in this table takes O(1)O(1) time; therefore, the time complexity of this approach will also be O(n×capacity)O(n \times capacity).

Space complexity#

We need O(n×capacity)O(n \times capacity) space in memory to store the intermediate values.

Can we do better?#

You must have noticed in the above algorithm that to fill up a row, we only require the previous rows’s values; that is, for filling the row against the ithi^{th} element, we require the values from the previous row representing the (i1)th(i-1)^{th} element. Therefore, there is no point in storing all the previous (i2)(i-2) rows.

We can further improve this approach by using a 1-D array of length (capacity+1)(capacity + 1) instead of creating a table. Next, we update this array for each element using the same calculation which we used earlier.

The time complexity would remain the same, O(n×capacity)O(n \times capacity), because we still have to do the calculation for each element. The space complexity, however, reduces to O(capacity)O(capacity) since we are only maintaining an array of size (capacity+1)(capacity + 1).

Introduction to 0/1 Knapsack

Target Sum